feat(lint): reject a source line address inside a test name - #8480
Conversation
A `File.tsx:123` address in a test NAME is read by nothing — not an assertion, no gate parses it, the cited file is never opened — so it cannot fail, and it rots the first time a line is inserted above what it cites. objectui#7853 ruled the class (cite by CONTENT, not by line address); five per-instance repairs followed it without closing it. Adds `object-ui/no-line-address-in-test-name` to the local plugin and wires it at `error` over test files, then converts the whole live population so it lints clean on the day it lands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
…thing Tooling plus test names only; empty frontmatter is the gate's own explicit exemption (scripts/check-changeset-presence.mjs). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
|
LANDED —
⭐ That last control is worth reading twice, and it corroborates the design rather than contradicting it. ⭐ What landed beyond the fix: the rule judges an address in any string that reaches a title — literal,
Generated by Claude Code |
Fixes #8047
Adds
object-ui/no-line-address-in-test-nameto the local ESLint plugin, wires it aterrorover test files, and converts the whole live population so it lints clean on the day it lands — the same shape as every otherobject-ui/*ratchet ineslint.config.js.A
File.tsx:123address inside a test name is read by nothing: it is not an assertion, no gate parses it, and the cited file is never opened. So it cannot fail. It rots the first time a line is inserted above what it cites, and the rot is invisible until a reader believes it. objectui#7853 ruled the class — cite the assertion by CONTENT, not by line address — and five per-instance repairs followed it without closing it.priority:p3.1. The population, re-derived on
origin/mainat868e82501⛔ The card's
3 / 5 more / 6is not today's figure and was not carried forward. Both instruments were re-run over the 2454 test files underpackages/, comments masked by the tree's ownscripts/js-comment-mask.mjs:it(lineThree readings the card did not have, and each one changes something:
page-header-authorable-keys.test.tsxcontaining the words "reads and draws it (PageHeader.tsx:123", whereit (merely looks like a declaration. So theit(-anchored sweep is wrong in both directions, not only short.it(names inpackages/plugin-charts/src/ObjectChart.absentCategoryAxisRefusal-8168.test.tsx, and 32 addresses reaching names throughit.each(CORPUS)('adopts $src …')in twoapp-shellmetadata-admin inspector tests — a different interpolation shape from the card's worked example, and one theit(-anchored instrument sees zero of.⇒ The card's central claim survives re-derivation and gets stronger: the narrow instrument misses 32 of 37.
2. Design question 1 — interpolated names. Settled: strings that REACH a title, decided on the AST
The rejected alternative was to judge genuinely resolved names by collecting them with the runtime (
vitest list --json). Disqualified twice:vitest list --jsonover oneplugin-chartsfile took 11.9 s; over oneapp-shelldomfile it had emitted nothing after nine minutes. There are 2454 test files.So the rule reads the AST, in three legs:
it.skip/it.each/describechaineachcase table, when the title interpolatesTable resolution deliberately over-approximates:
CORPUS.filter(fn)resolves to all ofCORPUS, because a filter can only drop rows and over-reporting is loud where under-reporting is silent. That is not hypothetical —ConditionBuilder.subjectVocabulary.test.tsxhas exactly that shape, and it is one of the 32.The worked example is caught.
it.each(roundTrippable)('adopts $src as structured rows', …)whereroundTrippable = CORPUS.filter((c) => !c.parens)andCORPUSis a 17-row table of{ src, cel }— the addresses never appear on anit(line. Reported: 16 per file, 32 total. ⭐ Andsrcthere is read by nothing else: both call sites destructure({ cel })only, so the field exists solely to be spliced into a name.3. Design question 2 — where a line address is legitimate. The boundary, and its cost
Drawn in the rule's own header, with the reason, and it is drawn on the title's own reach rather than on a keyword list. Three carve-outs, each with an instance measured in this tree today:
getAllComments.readme-app-shell-example.test.ts:231and:255,guide-layout-sidebar-nav-doc.test.ts:490, and the fourproducer:fields ingridNonAuthorKeys.test.tsxthat its line 229 splices into a failure message.page-header-authorable-keys.test.tsx'sRENDERER_OWN_DECLAREDrationale carries an address and is read — line 227 asserts on that very string. Something checks it, so it is not the unreadable class.The mechanism that makes carve-out 2 automatic: for
it.each(ROWS)('the spec refuses DOLLAR-key as an unrecognized key')only thekeyfield of a row can arrive in the name, so a siblingproducer:field is data. A positional title (%s,DOLLAR-0) or a template title cannot say which field arrives, so those read the whole row.it.each(covered)('%s declares …')inapps/console/src/__tests__/registry-inputs-spec-parity.test.ts, the one in-tree instance — is not covered. Reporting it would mean flagging a rationale record that the same file asserts on (carve-out 3, a false positive), and the remedy offered would be to hoist a table computed from the live component registry, which cannot be hoisted. One site today, written into the rule header so the next reader does not mistake silence there for coverage.4. Non-vacuity — the rule is proved able to fail, on each shape it claims
Every mutation was proved to have reached disk by HEAD blob hash versus worktree hash plus a marker count, before any result was read; every restore ran under an
EXIT INT TERMtrap with absolute paths and was proved by state (git diff HEADempty), never by exit code.Rule ablation (
eslint-rules/no-line-address-in-test-name.test.js, 12 valid + 11 invalid cases):Tests 23 passed (23), exit 0Tests 6 failed / 17 passed, exit 1it(-anchored reader (delete legs 2 and 3)Tests 5 failed / 18 passed, exit 1producer:carve-out redTests 1 failed / 22 passed, exit 1⭐ A2 is the one that matters. It rebuilds precisely the instrument that under-counted this class and shows it failing five cases the shipped rule passes — the blind spot demonstrated rather than asserted.
⭐ A3 proves the carve-outs are load-bearing, not decoration: widening the rule breaks a legitimate in-tree shape.
Tree-level ablation, against real code rather than fixtures — the three repaired files reverted to
868e82501and the rule re-run:Every mutation's landing was proved before its result was read, e.g.
LANDED plugin-charts/…-8168.test.tsx HEAD=4f70655867ef DISK=a01b3c097a1e addresses on disk: 5.5. The conversion — 37 sites, and one naming decision a reviewer should look at
packages/plugin-charts/src/ObjectChart.absentCategoryAxisRefusal-8168.test.tsx— 5 names, pure deletion. The content is already in the name (plugin-list ListView …) and the cited source lines are already quoted verbatim in the comment under eachit(. Names stay distinct.packages/app-shell/src/views/metadata-admin/inspectors/{ConditionBuilder.subjectVocabulary,FlowNodeConfigField.entryCondition}.test.tsx— 32 addresses. The:NNNhalf is deleted from eachsrcvalue, which keeps the provenance file and drops the moving target; these point into another repository's example apps, so nothing here could ever have checked them.app-shellreviewer.srcwas the corpus rows' only distinguishing field, so deleting the line number alone would collapse twelve case names to one.(corpus row DOLLAR-hash)was added to the twoadoptstitles to carry the identity the address had been carrying. That is a rewrite, not a deletion, and it is the reason these two files were not treated as a mechanical in-scope repair without saying so.6. Named consequences
domain:spec". It merged asd9788c14on 2026-09-07, andpackages/types/src/__tests__/text-value-retired-6951.test.tsis clean today: this rule does not red on it, so the conflict the brief prepared for does not exist. Nothing in that file was touched here.findinglabel only, no assignee, nodomain:*) for the reading objectui#8045 handed to this card: 27 published.describe()schema descriptions inpackages/types/src/zod/**cite source line addresses. Ruled out of scope here — a.describe()string is a published schema description reaching the authorable surface, so editing it is contract-adjacent, not a test rename.overlay.zod.tsanddata-display.zod.tsgrew the count when objectui#8354 / objectui#8405 landed.7. Verification
All exit codes captured before any pipe. Heavy runs through the shared verify lock, quoting its own verdict line. Measured at
39044bb61.no-line-address-in-test-name: 37 hits in 3 files0 hits; the census of every other rule's error count is unchanged (130 -> 93, delta only on this rule,37 -> 0)eslint.config.js, its own config object scoped**/*.test.{ts,tsx}and**/__tests__/**/*.{ts,tsx}, mirroring theno-dynamic-import-in-test-hookratchet blockturbo run lintrunseslint .per package)packages/plugin-charts eslint exit=0(0 errors) ·packages/app-shell eslint exit=0(0 errors)Test Files 4 passed (4)·Tests 114 passed (114)·VERDICT command-exit 0, root vitest confirmed (RUN v4.1.10 /home/user/objectui-issue-8047)eslint.config.jsandeslint-rules/**Test Files 15 passed (15)·Tests 349 passed (349)·VERDICT command-exit 0node scripts/check-changeset-presence.mjsnode scripts/check-changeset-no-major.mjsnode scripts/check-lint-rule-coverage.mjseslint-rules/**/*.jsledger rownode scripts/check-lint-coverage.mjs46/46 packages linted, 0 with outstanding errorsnode scripts/check-control-bytes.mjsnode scripts/check-governed-queue-guard.mjs --test(all 8 changed paths)NOT GOVERNED; self-test132 cases pass--no-inline-config, which is not whatturbo run lintruns. The 93 residual errors it shows are findings suppressed by in-file directives, not regressions — the CI-shaped per-package runs are 0 errors. The delta row is the load-bearing reading, and it is unaffected either way.Affected packages read from
turbo ls --affectedagainst868e82501, not guessed. Only test files changed in those packages, so running exactly the changed test files is a proved narrowing rather than a skipped one; CI runs the farm.四轴分析
实际业务需求. 服务的是真实场景,不是投机能力面。判据是实测而非"读起来有用":这个类在一条已有常设裁定(objectui#7853)之下仍然复发,今天在
origin/main上有 37 处活体,分布在 3 个文件里,其中ObjectChart.absentCategoryAxisRefusal-8168.test.tsx是卡片提出之后才落地的新增复发。同时,人工/agent 普查在这个类上结构性欠数:窄仪器看不见 37 处里的 32 处。⇒ 需求是被测出来的,不是被推断出来的。项目长远合理性. 契约优先,不是临时补丁。规则落在已存在的本地 ESLint 插件里,用已存在的接线(
eslint.config.js的 ratchet 段)和已存在的 RuleTester 约定,不新增 workflow、不新增 CI job、不新增 baseline 文件。⛔ 特别地:没有引入 baseline/grandfather 名单 —— 全部 37 处在同一个改动里转换完毕,符合本仓每一条object-ui/*ratchet 的既有措辞("existing sites pre-cleaned first, so the rule lints clean on the day it lands"),也符合维护者 2026-08-27「不设分阶段窗口」的裁定。防 AI 写代码犯错. 这条轴给出最强的支持。行地址型引用正是 AI 批量写测试时最容易产出、也最不可能被发现的东西:它读起来精确、写起来便宜、而且永远不会失败。声明即强制 —— 规则把"这条引用没有任何东西在读"从一句人类要记住的规程(objectui#7853,五次复发)变成一次写入时的响亮拒绝。⛔ 反面做法(在消费端加宽容、或让规则对看不懂的表保持沉默)已被明确拒绝:leg 3 存在的全部理由就是"看不出来"不得和"这里很干净"拼成同一个字。
创业阶段不扩散需求 —— 本轴默认反对新增门禁,必须正面回应. 反对的理由成立:
p3,今天没有任何东西是假的,新增守卫就是新增维护面。⇒ 逐条称重后仍然推荐落地,理由有三,且都是量出来的:① 边际维护成本近似为零 —— 一个规则文件加一个测试文件,复用既有插件、既有接线、既有测试框架,没有新 workflow、新 job、新脚本族、新 baseline;② 反对新增门禁的通常论据(没有实测成本、没有 population)在这里两条都不成立,分诊已经论证过,本轮重新取数后结论更强(37 处、复发仍在继续、窄仪器欠数 32/37);③ 本轴反对的是能力扩张,而这不是能力扩张 —— 它不新增任何可声明面、不新增任何可创作键、不给任何人新的东西可写,它只是把一条已经存在的裁定变得可执行。四轴冲突处的取舍,如实呈现给维护者: 唯一的张力在第四轴与前三轴之间,而它落在"一个 p3 的类值不值一个新守卫"上。本席的推荐是值,理由是上面的 ①②③;若维护者读到的权衡不同,合理的替代不是弱化规则,而是整个不落地并把 objectui#8047 关成
not_planned—— ⛔ 不要保留一个把 leg 2/leg 3 拿掉的"轻量版",那正是 A2 消融证明会重新打开盲区的那个形状。🤖 Generated with Claude Code
https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
Generated by Claude Code